import React, {Component, CSSProperties} from "react";
import ReactDOM from "react-dom";

interface StickyProps {
  topOffset: number;
  bottomOffset: number;
  relative: boolean;
  disableCompensation?: boolean;
  disableHardwareAcceleration?: boolean;
  children: StickyChildrenFunc;
  className?: string;
  style?: CSSProperties;
  extra?: any;
}

interface StickyState {
  isSticky: boolean;
  wasSticky: boolean;
  style: any;
  distanceFromTop?: number;
  distanceFromBottom?: number;
  calculatedHeight?: string;
  extra?: any;
}

type StickyChildrenFunc = (param: StickyState) => any;

export interface SubscribeProps {
  distanceFromTop: number;
  distanceFromBottom: number;
  eventSource: any;
}

export type SubscriptHandler = (props: SubscribeProps) => void;

class Sticky extends Component<StickyProps, StickyState> {

  static defaultProps = {
    relative: false,
    topOffset: 0,
    bottomOffset: 0,
    disableCompensation: false,
    disableHardwareAcceleration: false
  };

  static contextTypes = {
    subscribe: (props: any) => {
    },
    unsubscribe: (props: any) => {
    },
    getParent: (props: any) => {
    }
  };

  private placeholder: any;
  private content: any;

  constructor(props: StickyProps) {
    super(props);
    this.state = {
      isSticky: false,
      wasSticky: false,
      style: {}
    };
  }

  componentDidMount() {
    if (!this.context.subscribe) {
      throw new TypeError("Expected Sticky to be mounted within StickyContainer");
    }
    this.context.subscribe(this.handleContainerEvent);
  }

  componentWillUnmount() {
    this.context.unsubscribe(this.handleContainerEvent);
  }

  componentDidUpdate() {
    this.placeholder.style.paddingBottom = this.props.disableCompensation ? 0 : `${this.state.isSticky ? this.state.calculatedHeight : 0}px`;
  }

  handleContainerEvent = ({distanceFromTop, distanceFromBottom, eventSource}: SubscribeProps) => {
    const parent = this.context.getParent();
    let preventingStickyStateChanges = false;
    if (this.props.relative) {
      preventingStickyStateChanges = eventSource !== parent;
      distanceFromTop = -(eventSource.scrollTop + eventSource.offsetTop) + this.placeholder.offsetTop;
    }

    const placeholderClientRect = this.placeholder.getBoundingClientRect();
    const contentClientRect = this.content.getBoundingClientRect();
    const calculatedHeight = contentClientRect.height;

    const bottomDifference = distanceFromBottom - this.props.bottomOffset - calculatedHeight;

    const wasSticky = this.state.isSticky;
    const isSticky = preventingStickyStateChanges ? wasSticky : distanceFromTop <= -this.props.topOffset && distanceFromBottom > -this.props.bottomOffset;

    distanceFromBottom = (this.props.relative ? parent.scrollHeight - parent.scrollTop : distanceFromBottom) - calculatedHeight;

    const style: any = !isSticky ? {} : {
      position: "fixed",
      top: bottomDifference > 0 ? this.props.relative ? parent.getBoundingClientRect().top -
        parent.offsetParent.scrollTop : (this.props.topOffset || 0) * -1 : bottomDifference,
      left: placeholderClientRect.left,
      width: placeholderClientRect.width
    }

    if (!this.props.disableHardwareAcceleration) {
      style.transform = "translateZ(0)";
    }

    this.setState({
      isSticky,
      wasSticky,
      distanceFromTop,
      distanceFromBottom,
      calculatedHeight,
      style
    });
  };

  render() {
    const element = React.cloneElement(
      this.props.children({
        isSticky: this.state.isSticky,
        wasSticky: this.state.wasSticky,
        distanceFromTop: this.state.distanceFromTop,
        distanceFromBottom: this.state.distanceFromBottom,
        calculatedHeight: this.state.calculatedHeight,
        style: this.state.style,
        extra: this.props.extra
      }), {
        ref: (content: any) => {
          this.content = ReactDOM.findDOMNode(content);
        }
      }
    )

    const extra: any = {};
    if (this.props.className) {
      extra.className = this.props.className;
    }

    if (this.props.style) {
      extra.style = this.props.style;
    }

    return (
      <div {...extra}>
        <div ref={placeholder => (this.placeholder = placeholder)}/>
        {element}
      </div>
    );
  }
}

export default Sticky;
